#!/usr/bin/env python3

"""
This tool is for checking the security hardening options of the Linux kernel.

Author: Alexander Popov <alex.popov@linux.com>

This module performs unit-testing of the kernel-hardening-checker engine.
"""

# pylint: disable=missing-function-docstring,line-too-long

import unittest
import io
import sys
import json
import inspect
from typing import Union, Optional, List, Dict, Tuple
from .engine import StrOrBool, ChecklistObjType, KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck, OR, AND
from .engine import populate_with_data, perform_checks, override_expected_value


ResultType = List[Union[Dict[str, StrOrBool], str]]


class TestEngine(unittest.TestCase):
    """
    Example test scenario:

        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [KconfigCheck('reason_1', 'decision_1', 'KCONFIG_NAME', 'expected_1')]
        config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'cmdline_name', 'expected_2')]
        config_checklist += [SysctlCheck('reason_3', 'decision_3', 'sysctl_name', 'expected_3')]

        # 2. prepare the parsed kconfig options
        parsed_kconfig_options  = {}
        parsed_kconfig_options['CONFIG_KCONFIG_NAME'] = 'UNexpected_1'

        # 3. prepare the parsed cmdline options
        parsed_cmdline_options  = {}
        parsed_cmdline_options['cmdline_name'] = 'expected_2'

        # 4. prepare the parsed sysctl options
        parsed_sysctl_options  = {}
        parsed_sysctl_options['sysctl_name'] = 'expected_3'

        # 5. prepare the kernel version
        kernel_version = (42, 43, 44)

        # 6. run the engine
        self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, kernel_version)

        # 7. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(...
    """

    maxDiff = None

    @staticmethod
    def run_engine(checklist: List[ChecklistObjType],
                   parsed_kconfig_options: Optional[Dict[str, str]],
                   parsed_cmdline_options: Optional[Dict[str, str]],
                   parsed_sysctl_options: Optional[Dict[str, str]],
                   kernel_version: Optional[Tuple[int, int, int]]) -> None:
        # populate the checklist with data
        if parsed_kconfig_options:
            populate_with_data(checklist, parsed_kconfig_options, 'kconfig')
        if parsed_cmdline_options:
            populate_with_data(checklist, parsed_cmdline_options, 'cmdline')
        if parsed_sysctl_options:
            populate_with_data(checklist, parsed_sysctl_options, 'sysctl')
        if kernel_version:
            populate_with_data(checklist, kernel_version, 'version')

        # now everything is ready, perform the checks
        perform_checks(checklist)

        # print the table with the results
        print(f'\n{inspect.stack()[1].function}():')
        print('=' * 121)
        for opt in checklist:
            opt.table_print('verbose', True) # verbose mode, with_results
            print()
            print('=' * 121)

        # print the results in JSON
        result = []
        for opt in checklist:
            result.append(opt.json_dump(True)) # with_results
        print(json.dumps(result))
        print()

    @staticmethod
    def get_engine_result(checklist: List[ChecklistObjType], result: ResultType, result_type: str) -> None:
        assert(result_type in ('json', 'stdout', 'stdout_verbose')), \
               f'invalid result type "{result_type}"'

        if result_type == 'json':
            for opt in checklist:
                result.append(opt.json_dump(True)) # with_results
            return

        captured_output = io.StringIO()
        stdout_backup = sys.stdout
        sys.stdout = captured_output
        for opt in checklist:
            if result_type == 'stdout_verbose':
                opt.table_print('verbose', True) # verbose mode, with_results
            else:
                opt.table_print(None, True) # normal mode, with_results
        sys.stdout = stdout_backup
        result.append(captured_output.getvalue())

    def test_simple_kconfig(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1')]
        config_checklist += [KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2')]
        config_checklist += [KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3')]
        config_checklist += [KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'is not set')]
        config_checklist += [KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'is present')]
        config_checklist += [KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'is present')]
        config_checklist += [KconfigCheck('reason_7', 'decision_7', 'NAME_7', 'is not off')]
        config_checklist += [KconfigCheck('reason_8', 'decision_8', 'NAME_8', 'is not off')]
        config_checklist += [KconfigCheck('reason_9', 'decision_9', 'NAME_9', 'is not off')]
        config_checklist += [KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'is not off')]

        # 2. prepare the parsed kconfig options
        parsed_kconfig_options  = {}
        parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1'
        parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2'
        parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5'
        parsed_kconfig_options['CONFIG_NAME_7'] = 'really_not_off'
        parsed_kconfig_options['CONFIG_NAME_8'] = 'off'
        parsed_kconfig_options['CONFIG_NAME_9'] = '0'

        # 3. run the engine
        self.run_engine(config_checklist, parsed_kconfig_options, None, None, None)

        # 4. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_2', 'type': 'kconfig', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "UNexpected_2"', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_3', 'type': 'kconfig', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: is not found', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'is not set', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'OK: is not found', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_5', 'type': 'kconfig', 'desired_val': 'is present', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'OK: is present', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_6', 'type': 'kconfig', 'desired_val': 'is present', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: is not present', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_7', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: is not off, "really_not_off"', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_8', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_8', 'reason': 'reason_8', 'check_result': 'FAIL: is off', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_9', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'FAIL: is off, "0"', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_10', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}]
        )

    def test_simple_cmdline(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [CmdlineCheck('reason_1', 'decision_1', 'name_1', 'expected_1')]
        config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2')]
        config_checklist += [CmdlineCheck('reason_3', 'decision_3', 'name_3', 'expected_3')]
        config_checklist += [CmdlineCheck('reason_4', 'decision_4', 'name_4', 'is not set')]
        config_checklist += [CmdlineCheck('reason_5', 'decision_5', 'name_5', 'is present')]
        config_checklist += [CmdlineCheck('reason_6', 'decision_6', 'name_6', 'is present')]
        config_checklist += [CmdlineCheck('reason_7', 'decision_7', 'name_7', 'is not off')]
        config_checklist += [CmdlineCheck('reason_8', 'decision_8', 'name_8', 'is not off')]
        config_checklist += [CmdlineCheck('reason_9', 'decision_9', 'name_9', 'is not off')]
        config_checklist += [CmdlineCheck('reason_10', 'decision_10', 'name_10', 'is not off')]

        # 2. prepare the parsed cmdline options
        parsed_cmdline_options  = {}
        parsed_cmdline_options['name_1'] = 'expected_1'
        parsed_cmdline_options['name_2'] = 'UNexpected_2'
        parsed_cmdline_options['name_5'] = ''
        parsed_cmdline_options['name_7'] = ''
        parsed_cmdline_options['name_8'] = 'off'
        parsed_cmdline_options['name_9'] = '0'

        # 3. run the engine
        self.run_engine(config_checklist, None, parsed_cmdline_options, None, None)

        # 4. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'name_1', 'type': 'cmdline', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "UNexpected_2"', 'check_result_bool': False},
                 {'option_name': 'name_3', 'type': 'cmdline', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: is not found', 'check_result_bool': False},
                 {'option_name': 'name_4', 'type': 'cmdline', 'desired_val': 'is not set', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'OK: is not found', 'check_result_bool': True},
                 {'option_name': 'name_5', 'type': 'cmdline', 'desired_val': 'is present', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'OK: is present', 'check_result_bool': True},
                 {'option_name': 'name_6', 'type': 'cmdline', 'desired_val': 'is present', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: is not present', 'check_result_bool': False},
                 {'option_name': 'name_7', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: is not off, ""', 'check_result_bool': True},
                 {'option_name': 'name_8', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_8', 'reason': 'reason_8', 'check_result': 'FAIL: is off', 'check_result_bool': False},
                 {'option_name': 'name_9', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'FAIL: is off, "0"', 'check_result_bool': False},
                 {'option_name': 'name_10', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}]
        )

    def test_simple_sysctl(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [SysctlCheck('reason_1', 'decision_1', 'name_1', 'expected_1')]
        config_checklist += [SysctlCheck('reason_2', 'decision_2', 'name_2', 'expected_2')]
        config_checklist += [SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3')]
        config_checklist += [SysctlCheck('reason_4', 'decision_4', 'name_4', 'is not set')]
        config_checklist += [SysctlCheck('reason_5', 'decision_5', 'name_5', 'is present')]
        config_checklist += [SysctlCheck('reason_6', 'decision_6', 'name_6', 'is present')]
        config_checklist += [SysctlCheck('reason_7', 'decision_7', 'name_7', 'is not off')]
        config_checklist += [SysctlCheck('reason_8', 'decision_8', 'name_8', 'is not off')]
        config_checklist += [SysctlCheck('reason_9', 'decision_9', 'name_9', 'is not off')]
        config_checklist += [SysctlCheck('reason_10', 'decision_10', 'name_10', 'is not off')]

        # 2. prepare the parsed sysctl options
        parsed_sysctl_options  = {}
        parsed_sysctl_options['name_1'] = 'expected_1'
        parsed_sysctl_options['name_2'] = 'UNexpected_2'
        parsed_sysctl_options['name_5'] = ''
        parsed_sysctl_options['name_7'] = ''
        parsed_sysctl_options['name_8'] = 'off'
        parsed_sysctl_options['name_9'] = '0'

        # 3. run the engine
        self.run_engine(config_checklist, None, None, parsed_sysctl_options, None)

        # 4. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'name_1', 'type': 'sysctl', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'name_2', 'type': 'sysctl', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "UNexpected_2"', 'check_result_bool': False},
                 {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: is not found', 'check_result_bool': False},
                 {'option_name': 'name_4', 'type': 'sysctl', 'desired_val': 'is not set', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'OK: is not found', 'check_result_bool': True},
                 {'option_name': 'name_5', 'type': 'sysctl', 'desired_val': 'is present', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'OK: is present', 'check_result_bool': True},
                 {'option_name': 'name_6', 'type': 'sysctl', 'desired_val': 'is present', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: is not present', 'check_result_bool': False},
                 {'option_name': 'name_7', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: is not off, ""', 'check_result_bool': True},
                 {'option_name': 'name_8', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_8', 'reason': 'reason_8', 'check_result': 'FAIL: is off', 'check_result_bool': False},
                 {'option_name': 'name_9', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'FAIL: is off, "0"', 'check_result_bool': False},
                 {'option_name': 'name_10', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}]
        )

    def test_complex_or(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
                                KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'))]
        config_checklist += [OR(KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3'),
                                KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'))]
        config_checklist += [OR(KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'expected_5'),
                                KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'expected_6'))]
        config_checklist += [OR(KconfigCheck('reason_7', 'decision_7', 'NAME_7', 'expected_7'),
                                KconfigCheck('reason_8', 'decision_8', 'NAME_8', 'is not set'))]
        config_checklist += [OR(KconfigCheck('reason_9', 'decision_9', 'NAME_9', 'expected_9'),
                                KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'is present'))]
        config_checklist += [OR(KconfigCheck('reason_11', 'decision_11', 'NAME_11', 'expected_11'),
                                KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'is not off'))]

        # 2. prepare the parsed kconfig options
        parsed_kconfig_options  = {}
        parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1'
        parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2'
        parsed_kconfig_options['CONFIG_NAME_3'] = 'UNexpected_3'
        parsed_kconfig_options['CONFIG_NAME_4'] = 'expected_4'
        parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5'
        parsed_kconfig_options['CONFIG_NAME_6'] = 'UNexpected_6'
        parsed_kconfig_options['CONFIG_NAME_10'] = 'UNexpected_10'
        parsed_kconfig_options['CONFIG_NAME_12'] = 'really_not_off'

        # 3. run the engine
        self.run_engine(config_checklist, parsed_kconfig_options, None, None, None)

        # 4. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_3', 'type': 'kconfig', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'OK: CONFIG_NAME_4 is "expected_4"', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_5', 'type': 'kconfig', 'desired_val': 'expected_5', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'FAIL: "UNexpected_5"', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_7', 'type': 'kconfig', 'desired_val': 'expected_7', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: CONFIG_NAME_8 is not found', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_9', 'type': 'kconfig', 'desired_val': 'expected_9', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'OK: CONFIG_NAME_10 is present', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_11', 'type': 'kconfig', 'desired_val': 'expected_11', 'decision': 'decision_11', 'reason': 'reason_11', 'check_result': 'OK: CONFIG_NAME_12 is not off', 'check_result_bool': True}]
        )

    def test_complex_and(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [AND(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
                                 KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'))]
        config_checklist += [AND(KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3'),
                                 KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'))]
        config_checklist += [AND(KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'expected_5'),
                                 KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'expected_6'))]
        config_checklist += [AND(KconfigCheck('reason_7', 'decision_7', 'NAME_7', 'expected_7'),
                                 KconfigCheck('reason_8', 'decision_8', 'NAME_8', 'is present'))]
        config_checklist += [AND(KconfigCheck('reason_9', 'decision_9', 'NAME_9', 'expected_9'),
                                 KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'is not off'))]
        config_checklist += [AND(KconfigCheck('reason_11', 'decision_11', 'NAME_11', 'expected_11'),
                                 KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'is not off'))]

        # 2. prepare the parsed kconfig options
        parsed_kconfig_options  = {}
        parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1'
        parsed_kconfig_options['CONFIG_NAME_2'] = 'expected_2'
        parsed_kconfig_options['CONFIG_NAME_3'] = 'expected_3'
        parsed_kconfig_options['CONFIG_NAME_4'] = 'UNexpected_4'
        parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5'
        parsed_kconfig_options['CONFIG_NAME_6'] = 'expected_6'
        parsed_kconfig_options['CONFIG_NAME_7'] = 'expected_7'
        parsed_kconfig_options['CONFIG_NAME_9'] = 'expected_9'
        parsed_kconfig_options['CONFIG_NAME_10'] = '0'
        parsed_kconfig_options['CONFIG_NAME_11'] = 'expected_11'

        # 3. run the engine
        self.run_engine(config_checklist, parsed_kconfig_options, None, None, None)

        # 4. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_3', 'type': 'kconfig', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: CONFIG_NAME_4 is not "expected_4"', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_5', 'type': 'kconfig', 'desired_val': 'expected_5', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'FAIL: "UNexpected_5"', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_7', 'type': 'kconfig', 'desired_val': 'expected_7', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'FAIL: CONFIG_NAME_8 is not present', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_9', 'type': 'kconfig', 'desired_val': 'expected_9', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'FAIL: CONFIG_NAME_10 is off', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_11', 'type': 'kconfig', 'desired_val': 'expected_11', 'decision': 'decision_11', 'reason': 'reason_11', 'check_result': 'FAIL: CONFIG_NAME_12 is off, not found', 'check_result_bool': False}]
        )

    def test_complex_nested(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [AND(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
                                 OR(KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'),
                                    KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3')))]
        config_checklist += [AND(KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'),
                                 OR(KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'expected_5'),
                                    KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'expected_6')))]
        config_checklist += [OR(KconfigCheck('reason_7', 'decision_7', 'NAME_7', 'expected_7'),
                                 AND(KconfigCheck('reason_8', 'decision_8', 'NAME_8', 'expected_8'),
                                     KconfigCheck('reason_9', 'decision_9', 'NAME_9', 'expected_9')))]
        config_checklist += [OR(KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'expected_10'),
                                 AND(KconfigCheck('reason_11', 'decision_11', 'NAME_11', 'expected_11'),
                                     KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'expected_12')))]

        # 2. prepare the parsed kconfig options
        parsed_kconfig_options  = {}
        parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1'
        parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2'
        parsed_kconfig_options['CONFIG_NAME_3'] = 'expected_3'
        parsed_kconfig_options['CONFIG_NAME_4'] = 'expected_4'
        parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5'
        parsed_kconfig_options['CONFIG_NAME_6'] = 'UNexpected_6'
        parsed_kconfig_options['CONFIG_NAME_7'] = 'UNexpected_7'
        parsed_kconfig_options['CONFIG_NAME_8'] = 'expected_8'
        parsed_kconfig_options['CONFIG_NAME_9'] = 'expected_9'
        parsed_kconfig_options['CONFIG_NAME_10'] = 'UNexpected_10'
        parsed_kconfig_options['CONFIG_NAME_11'] = 'UNexpected_11'
        parsed_kconfig_options['CONFIG_NAME_12'] = 'expected_12'

        # 3. run the engine
        self.run_engine(config_checklist, parsed_kconfig_options, None, None, None)

        # 4. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'expected_4', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'FAIL: CONFIG_NAME_5 is not "expected_5"', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_7', 'type': 'kconfig', 'desired_val': 'expected_7', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: CONFIG_NAME_8 is "expected_8"', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_10', 'type': 'kconfig', 'desired_val': 'expected_10', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: "UNexpected_10"', 'check_result_bool': False}]
        )

    def test_version(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
                                VersionCheck((41, 101, 0)))]
        config_checklist += [AND(KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'),
                                 VersionCheck((43, 1, 0)))]
        config_checklist += [OR(KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3'),
                                VersionCheck((42, 42, 101)))]
        config_checklist += [AND(KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'),
                                 VersionCheck((42, 44, 1)))]
        config_checklist += [OR(KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'expected_5'),
                                VersionCheck((42, 43, 44)))]
        config_checklist += [AND(KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'expected_6'),
                                 VersionCheck((42, 43, 45)))]

        # 2. prepare the parsed kconfig options
        parsed_kconfig_options  = {}
        parsed_kconfig_options['CONFIG_NAME_2'] = 'expected_2'
        parsed_kconfig_options['CONFIG_NAME_4'] = 'expected_4'
        parsed_kconfig_options['CONFIG_NAME_6'] = 'expected_6'

        # 3. prepare the kernel version
        kernel_version = (42, 43, 44)

        # 4. run the engine
        self.run_engine(config_checklist, parsed_kconfig_options, None, None, kernel_version)

        # 5. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK: version >= (41, 101, 0)', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_2', 'type': 'kconfig', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: version < (43, 1, 0)', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_3', 'type': 'kconfig', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'OK: version >= (42, 42, 101)', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'expected_4', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'FAIL: version < (42, 44, 1)', 'check_result_bool': False},
                 {'option_name': 'CONFIG_NAME_5', 'type': 'kconfig', 'desired_val': 'expected_5', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'OK: version >= (42, 43, 44)', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_6', 'type': 'kconfig', 'desired_val': 'expected_6', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: version < (42, 43, 45)', 'check_result_bool': False}]
        )

    def test_stdout(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
                                CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2'),
                                SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3'))]
        config_checklist += [AND(KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'),
                                 CmdlineCheck('reason_5', 'decision_5', 'name_5', 'expected_5'),
                                 SysctlCheck('reason_6', 'decision_6', 'name_6', 'expected_6'))]

        # 2. prepare the parsed kconfig options
        parsed_kconfig_options  = {}
        parsed_kconfig_options['CONFIG_NAME_1'] = 'UNexpected_1'

        # 3. prepare the parsed cmdline options
        parsed_cmdline_options  = {}
        parsed_cmdline_options['name_2'] = 'expected_2'
        parsed_cmdline_options['name_5'] = 'UNexpected_5'

        # 4. prepare the parsed sysctl options
        parsed_sysctl_options  = {}
        parsed_sysctl_options['name_6'] = 'expected_6'

        # 5. run the engine
        self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, None)

        # 6. check that the results are correct
        json_result = [] # type: ResultType
        self.get_engine_result(config_checklist, json_result, 'json')
        self.assertEqual(
                json_result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK: name_2 is "expected_2"', 'check_result_bool': True},
                 {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'expected_4', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'FAIL: name_5 is not "expected_5"', 'check_result_bool': False}]
        )

        stdout_result = [] # type: ResultType
        self.get_engine_result(config_checklist, stdout_result, 'stdout')
        self.assertEqual(
                stdout_result,
                [
'\
CONFIG_NAME_1                           |kconfig| expected_1 |decision_1|     reason_1     | OK: name_2 is "expected_2"\
CONFIG_NAME_4                           |kconfig| expected_4 |decision_4|     reason_4     | FAIL: name_5 is not "expected_5"\
'               ]
        )

        stdout_result = []
        self.get_engine_result(config_checklist, stdout_result, 'stdout_verbose')
        self.assertEqual(
                stdout_result,
                [
'\
    <<< OR >>>                                                                             | OK: name_2 is "expected_2"\n\
CONFIG_NAME_1                           |kconfig| expected_1 |decision_1|     reason_1     | FAIL: "UNexpected_1"\n\
name_2                                  |cmdline| expected_2 |decision_2|     reason_2     | OK\n\
name_3                                  |sysctl | expected_3 |decision_3|     reason_3     | None\
'\
'\
    <<< AND >>>                                                                            | FAIL: name_5 is not "expected_5"\n\
CONFIG_NAME_4                           |kconfig| expected_4 |decision_4|     reason_4     | None\n\
name_5                                  |cmdline| expected_5 |decision_5|     reason_5     | FAIL: "UNexpected_5"\n\
name_6                                  |sysctl | expected_6 |decision_6|     reason_6     | OK\
'               ]
        )

    def test_value_overriding(self) -> None:
        # 1. prepare the checklist
        config_checklist = [] # type: List[ChecklistObjType]
        config_checklist += [KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1')]
        config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2')]
        config_checklist += [SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3')]

        # 2. prepare the parsed kconfig options
        parsed_kconfig_options  = {}
        parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1_new'

        # 3. prepare the parsed cmdline options
        parsed_cmdline_options  = {}
        parsed_cmdline_options['name_2'] = 'expected_2_new'

        # 4. prepare the parsed sysctl options
        parsed_sysctl_options  = {}
        parsed_sysctl_options['name_3'] = 'expected_3_new'

        # 5. run the engine
        self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, None)

        # 6. check that the results are correct
        result = [] # type: ResultType
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'FAIL: "expected_1_new"', 'check_result_bool': False},
                 {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "expected_2_new"', 'check_result_bool': False},
                 {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: "expected_3_new"', 'check_result_bool': False}]
        )

        # 7. override expected value and perform the checks again
        override_expected_value(config_checklist, 'CONFIG_NAME_1', 'expected_1_new')
        perform_checks(config_checklist)

        # 8. check that the results are correct
        result = []
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1_new', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "expected_2_new"', 'check_result_bool': False},
                 {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: "expected_3_new"', 'check_result_bool': False}]
        )

        # 9. override expected value and perform the checks again
        override_expected_value(config_checklist, 'name_2', 'expected_2_new')
        perform_checks(config_checklist)

        # 10. check that the results are correct
        result = []
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1_new', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2_new', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: "expected_3_new"', 'check_result_bool': False}]
        )

        # 11. override expected value and perform the checks again
        override_expected_value(config_checklist, 'name_3', 'expected_3_new')
        perform_checks(config_checklist)

        # 12. check that the results are correct
        result = []
        self.get_engine_result(config_checklist, result, 'json')
        self.assertEqual(
                result,
                [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1_new', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2_new', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'OK', 'check_result_bool': True},
                 {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3_new', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'OK', 'check_result_bool': True}]
        )
